/*
*   The purpose of this file is to support netlists that
*   contain new types of hard blocks. Specifically, user defined
*   hard blocks are identified within the netlist and then instantiated
*   so that they are later written to the generated .blif file.
*
*   For example, consider a user design that contains a new hard block
*   called "router". This hard block is embedded within the FPGA architecture
*   but the Quartus synthesis tool has no reference to what a "router" block is.
*   Since the tool has no information to the hard block, the generated netlist
*   does not properly model the hard blocks. Instead of declaring a single hard 
*   block in the netlist, it was found that the generated .vqm netlist
*   from Quartus inserted a LUT for every input port of the hard block
*   and then a LUT & DFF for every output port of the hard block.
*   An example is shown below:
*
*   Suppose we had the following module in the design (representing a hard
*   block):
*
*       router router_1 (
*            .input_one  (pin_1), 
*            .input_two  (pin_2),
*            .output_one (pin_3),
*            .output_two (pin_4),
*        );
*
*   In the generated netlist from Quartus, the model will be represented as:
*       
*                                   router_1
*       --------------------------------------------------------------------
*       |            LUT                                                   |
*       |        --------------                                            |
*     pin_1 ---> input_one    |                                            |
*       |        |            |                                            |
*       |        |            |                                            |
*       |        --------------                                            |
*       |                                                                  |
*       |                                                                  |
*       |                                                                  |    
*       |            LUT                                                   |
*       |        --------------                                            |
*     pin_2 ---> input_two    |                                            |
*       |        |            |                                            |
*       |        |            |                                            |
*       |        --------------                                            |
*       |                                                                  |
*       |                                                                  |
*       |                                                                  |
*       |                           LUT                     DFF            |
*       |                       -----------            --------------      |
*       |                       |  output_one --------->           ------> pin_3
*       |                       |         |            |            |      |
*       |                       |         |            |            |      |
*       |                       -----------            --------------      |
*       |                                                                  |
*       |                                                                  |
*       |                                                                  |
*       |                           LUT                     DFF            |
*       |                       -----------            --------------      |
*       |                       |  output_two --------->           ------> pin_4
*       |                       |         |            |            |      |
*       |                       |         |            |            |      |
*       |                       -----------            --------------      |
*       --------------------------------------------------------------------
*
*       Looking at the figure above, the netlist is incorrect. Instead of 
*       of a single "black box" model, LUTS and DFF have been  
*       incorrectly added to represent all the "ports" of the hard block.
*
*       The functions in this file process the .vqm netlist generated by 
*       Quartus synthesis to fix the issues mentioned above.
*       While processing the netlist, if a LUT or DFF 
*       is found, then it is "checked" too see whether it represents a 
*       a hard block "port", if it does then the corresponding hard block
*       the "port" belongs to is identified and 
*       added to the netlist (newly instantiated). The corresponding luts and
*       flip flops are then removed from the netlist and their netlist
*       connections are then appropriately moved to the newly instantiated
*       hard block. 
*
*       For example, if we take the netlist with the "router" block
*       above and we process it using the functions in this file, the
*       router block in the resulting netlist will look as follows:
*                                   
*                                   router_1
*       --------------------------------------------------------------------
*       |                                                                  |
*       |                                                                  |
*     pin_1 ---> input_one                                                 |
*       |                                                                  |
*       |                                                                  |
*       |                                                                  |
*       |                                                                  |
*       |                                                                  |
*       |                                                                  |    
*       |                                                                  |
*       |                                                                  |
*     pin_2 ---> input_two                                                 |
*       |                                                                  |
*       |                                                                  |
*       |                                                                  |
*       |                                                                  |
*       |                                                                  |
*       |                                                                  |
*       |                                                                  |
*       |                                                                  |
*       |                                               output_one ------> pin_3
*       |                                                                  |
*       |                                                                  |
*       |                                                                  |
*       |                                                                  |
*       |                                                                  |
*       |                                                                  |
*       |                                                                  |
*       |                                                                  |
*       |                                               output_two ------> pin_4
*       |                                                                  |
*       |                                                                  | 
*       |                                                                  |
*       --------------------------------------------------------------------
*
*       Now the hard block is correctly modelled within the netlist. 
*/


#include "hard_block_recog.h"
#include "logic_types.h"

//============================================================================================
//			INTERNAL FUNCTION DECLARATIONS
//============================================================================================

static void initialize_hard_block_models(t_arch* main_arch, std::vector<std::string>* hard_block_type_names, t_hard_block_recog* storage_of_hard_block_info);

static void process_module_nodes_and_create_hard_blocks(t_module* main_module, std::vector<std::string>* hard_block_type_name_list, t_hard_block_recog* module_hard_block_node_refs_and_info);

static bool create_and_initialize_all_hard_block_ports(const t_model& hard_block_arch_model, t_hard_block_recog* storage_of_hard_block_info);

static void create_hard_block_port_info_structure(t_hard_block_recog* storage_of_hard_block_info, std::string hard_block_type_name);

static int extract_and_store_hard_block_model_ports(t_hard_block_recog* storage_of_hard_block_info, t_model_ports* curr_hard_block_model_port, std::string curr_hard_block_type_name,int port_index, std::string port_type);

static t_array_ref* convert_hard_block_model_port_to_hard_block_node_port(t_model_ports* hard_block_model_port);

static t_node_port_association* create_unconnected_node_port_association(char *port_name, int port_index, int wire_index);

static void store_hard_block_port_info(t_hard_block_recog* storage_of_hard_block_port_info, std::string curr_hard_block_type_name,std::string curr_port_name, t_array_ref** curr_port_array, int* port_index);

static void copy_array_ref(t_array_ref* array_ref_orig, t_array_ref* array_ref_copy);

static t_array_ref* create_and_initialize_t_array_ref_struct(void);

static int find_hard_block_instance(t_hard_block_recog* module_hard_block_node_refs_and_info, t_parsed_hard_block_port_info* curr_module_node_info);

static void assign_net_to_hard_block_instance_port(t_node* curr_module_node, t_parsed_hard_block_port_info* curr_module_node_info, t_hard_block_recog* module_hard_block_node_refs_and_info, int curr_hard_block_instance_index);

static t_node_port_association* get_lut_dffeas_port_connected_to_hard_block_instance_net(t_node* curr_module_node, DeviceInfo target_device_info);

static int identify_port_index_within_hard_block_type_port_array(t_hard_block_port_info* curr_hard_block_type_port_info, t_parsed_hard_block_port_info* curr_module_node_info, t_node* curr_module_node);

static void handle_net_assignment(t_node* curr_module_node, t_hard_block* curr_hard_block_instance, int port_to_assign_index, t_node_port_association* port_connected_to_hard_block_instance_net, t_parsed_hard_block_port_info* curr_module_node_info);

static bool is_hard_block_port_legal(t_node* curr_module_node, DeviceInfo target_device_info);

static int create_new_hard_block_instance(t_array_ref* module_node_list, t_hard_block_recog* module_hard_block_node_refs_and_info, t_parsed_hard_block_port_info* curr_module_node_info);

static t_array_ref* create_unconnected_hard_block_instance_ports(t_hard_block_port_info* curr_hard_block_type_port_info);

static t_node* create_new_hard_block_instance_node(t_array_ref* curr_hard_block_instance_ports, t_parsed_hard_block_port_info* curr_hard_block_instance_info);

static int store_new_hard_block_instance_info(t_hard_block_recog* module_hard_block_node_refs_and_info, t_hard_block_port_info* curr_hard_block_type_port_info, t_node* new_hard_block_instance_node, t_parsed_hard_block_port_info* curr_module_node_info);

static t_array_ref* create_t_array_ref_from_array(void** array_to_store, int array_size);

static void delete_hard_block_port_info(std::unordered_map<std::string, t_hard_block_port_info>* hard_block_type_name_to_port_info_map);

static t_parsed_hard_block_port_info extract_hard_block_port_info_from_module_node(t_node* curr_module_node, std::vector<std::string>* hard_block_type_name_list);

static std::string identify_hard_block_type(std::vector<std::string>* hard_block_type_name_list, std::string curr_node_name_component);

static void identify_hard_block_port_name_and_index (t_parsed_hard_block_port_info* curr_hard_block_port, std::string curr_node_name_component);

static void split_node_name(std::string original_node_name, std::vector<std::string>* node_name_components, std::string delimiter);

static std::string construct_hard_block_name(std::vector<std::string>*node_name_components, std::string delimiter);

static void remove_luts_dffeas_nodes_representing_hard_block_ports(t_module* main_module, t_hard_block_recog* module_hard_block_node_refs_and_info);

static void verify_hard_blocks(t_hard_block_recog* module_hard_block_node_refs_and_info);

// utility functions

bool sort_hard_blocks_by_valid_connections(t_hard_block, t_hard_block);

//============================================================================================
//============================================================================================

/**
 * @details This function is the main controller and executes all the
 *          processing steps involved in indentifying new types of hard 
 *          blocks within the design and adding them to the netlist. 
 * 
 *          The following steps are performed:
 *              - The FPGA architecture is parsed to internally model
 *                each hard block in the design
 *              - The nodes within the netlist are individually processed
 *                to determine whether hard blocks are included within the 
 *                design and then added to the netlist
 *              - The newly added hard blocks are verified to make sure they
 *                are legal
 *              - The luts and dffeas within the netlist that represented
 *                hard block ports are removed 
 * 
 * @param main_module This contains all the netlist information, such as luts,
 *                    flip flops, and other types of blocks. All the nets within
 *                    the netlist are also included.
 * 
 * @param main_arch This contains all the information regarding the FPGA
 *                  architecture that the design will be mapped to. 
 * 
 * @param list_hard_block_type_names A list of the hard block names that need
 *                                   to be properly added to the netlist.
 * 
 * @param arch_file_name Name of the architecture file that contains the FPGA
 *                       architecture information.
 * 
 * @param vqm_file_name Name of the quartus generated .vqm netlist file. 
 */
void add_hard_blocks_to_netlist(t_module* main_module, t_arch* main_arch, std::vector<std::string>* list_hard_block_type_names, std::string arch_file_name, std::string vqm_file_name, std::string device)
{
    t_hard_block_recog module_hard_block_node_refs_and_info;

    // variables used for reporting statistics to the user
    int number_of_hard_blocks_added = 0;
    int number_of_luts_flip_flops_removed = 0;

    // based on the device supplied, store the corresponding parameters related to that device
    module_hard_block_node_refs_and_info.target_device_info = (device_parameter_database.find(device))->second;

    /*
        We catch any errors that occur during the initialization procedure.
        Some errors that can occur are when the provided hard block names
        from the user were not found in the architecture file or the hard
        block models in the architecture file do not have any ports. 
        All the errors in this step are related to the architecture file, so
        once we catch the error, we append the architecture file location and
        throw another error to force program termination.
    */
    try
    {

        std::cout << "\t>> Generating models for custom hard blocks.\n";

        // internally model the new types of hard blocks using the architecture
        // file
        initialize_hard_block_models(main_arch, list_hard_block_type_names, &module_hard_block_node_refs_and_info);
    }
    catch(const vtr::VtrError& error)
    {
        throw vtr::VtrError((std::string)error.what() + " The FPGA architecture is described in " + arch_file_name + ".");
    }

    /*
        We catch any errors that occur during the procedure of reading the
        netlist and inserting custom hard blocks whereever necessary. 
        All the errors in this step are related to the provided .vqm netlist file, so once we catch the error, we append the netlist file location
        and throw another error to force program termination.
    */
    try
    {

        std::cout << "\t>> Processing netlist to infer and add custom hard blocks.\n";


        // go through the netlist nodes and add the new types of hard blocks
        // if they are needed
        process_module_nodes_and_create_hard_blocks(main_module, list_hard_block_type_names, &module_hard_block_node_refs_and_info);

        std::cout << "\t>> Verifying newly created custom hard blocks.\n";

        verify_hard_blocks(&module_hard_block_node_refs_and_info);

        number_of_hard_blocks_added = module_hard_block_node_refs_and_info.hard_block_instances.size();
        number_of_luts_flip_flops_removed = module_hard_block_node_refs_and_info.luts_dffeas_nodes_to_remove.size();

        std::cout << "\t>> Inferred and added " << number_of_hard_blocks_added << " hard blocks to the netlist.\n";
    }
    catch(const vtr::VtrError& error)
    {
        throw vtr::VtrError((std::string)error.what() + " The original netlist is described in " + vqm_file_name + ".");
    }

    std::cout << "\t>> Cleaning up netlist after adding hard blocks.\n";

    // at this point we have a list of luts/dffeas nodes found in the module that we need to remove
    // so we remove them here
    remove_luts_dffeas_nodes_representing_hard_block_ports(main_module, &module_hard_block_node_refs_and_info);

    std::cout << "\t>> Removed " << number_of_luts_flip_flops_removed << " luts and flip flops from the netlist.\n";

    // need to delete all the dynamic memory used to store 
    // all the hard block port information
    delete_hard_block_port_info(&(module_hard_block_node_refs_and_info.hard_block_type_name_to_port_info));

    return;
}

/**
 * @details Given a list of hard block names, an FPGA architecture is
 *          parsed to find and store information related to the hard blocks
 *          within the list. The ports and their indexing is stored.
 * 
 * @param main_arch This contains all the information regarding the FPGA
 *                  architeture that the design will be mapped to.
 * 
 * @param hard_block_type_names A list of the hard block names that need
 *                              to be properly added to the netlist.
 * 
 * @param storage_of_hard_block_info This is a data structure of type
 *                                   't_hard_block_recog', which can be found
 *                                   in 'hard_block_recog.h'. The port
 *                                   information of the hard blocks are stored
 *                                   in here.
 */
static void initialize_hard_block_models(t_arch* main_arch, std::vector<std::string>* hard_block_type_names, t_hard_block_recog* storage_of_hard_block_info)
{
    std::vector<std::string>::iterator hard_block_type_name_traverser;
    bool single_hard_block_init_result = false;

    // iterate through each hard block name within the list
    for (hard_block_type_name_traverser = hard_block_type_names->begin(); hard_block_type_name_traverser != hard_block_type_names->end(); hard_block_type_name_traverser++)
    {
        // get the corresponding model for each hard block name
        LogicalModelId hard_block_model_id = main_arch->models.get_model_by_name(*hard_block_type_name_traverser);

        // a check to see if the model was found within the FPGA architecture
        if (!hard_block_model_id.is_valid())
        {         
            throw vtr::VtrError("The provided hard block model: '" + *hard_block_type_name_traverser + "' was not found within the corresponding FPGA architecture.");
        }
        else
        {
            const t_model& hard_block_model = main_arch->models.get_model(hard_block_model_id);
            // store the port information for the current hard block model
            single_hard_block_init_result =  create_and_initialize_all_hard_block_ports(hard_block_model, storage_of_hard_block_info);

            // a check to make sure the hard block has both input and output ports
            if (!single_hard_block_init_result)
            {
                throw vtr::VtrError("Hard block model: '" + *hard_block_type_name_traverser + "' found in the architecture has no input/output ports.");
            }
        }

    }

    return;

}

/**
 * @details Given a module (which is the netlist), iterate through each 
 *          node to determine whether the node represents a hard block port.
 *          If the node does represent a port, then a new hard block node
 *          is created and added to the module and the corresponding port
 *          is connected to the same net as the node that represented that port.
 *          If the hard block was already created, then it is not created again.
 *          Finally the nodes that were identified as representing hard block
 *          ports are stored for reference. The nodes are stored so that they
 *          can be removed from the netlist and deleted, since they are replaced
 *          by the corresponding hard blocks themselves.
 *  
 * @param main_module This contains all the netlist information, such as luts,
 *                    flip flops, and other types of blocks. All the nets within
 *                    the netlist are also included.
 * 
 * @param hard_block_type_name_list A list of the hard block names that need
 *                              to be properly added to the netlist.
 * 
 * @param module_hard_block_node_refs_and_info This is a data structure of type
 *                                     't_hard_block_recog', which can be found
 *                                     in 'hard_block_recog.h'. The port
 *                                     information of the hard blocks are stored
 *                                     in here.
 */
static void process_module_nodes_and_create_hard_blocks(t_module* main_module, std::vector<std::string>* hard_block_type_name_list, t_hard_block_recog* module_hard_block_node_refs_and_info)
{   
    // represents a block in the netlist
    // refer to 'vqm_dll.h' for more info on t_node
    t_node* curr_module_node = NULL;

    // create a new t_array_ref (refer to 'vqm_dll.h') structure so that we can append new hard block instances we create to the original node array
    t_array_ref* node_list_with_hard_blocks = create_t_array_ref_from_array((void**)(main_module->array_of_nodes), main_module->number_of_nodes);

    std::string curr_module_node_type = "";
    std::string curr_node_name = "";
    
    int number_of_module_nodes = main_module->number_of_nodes; // before any hard blocks are added

    t_parsed_hard_block_port_info curr_module_node_info;

    int curr_hard_block_instance_index = 0;

    /* iterate through every node in the module and create a new node to represent each and every hard block we identify within the netlist.*/
    for (int i = 0; i < number_of_module_nodes; i++)
    {

        curr_module_node = (t_node*)node_list_with_hard_blocks->pointer[i];
        curr_module_node_type.assign(curr_module_node->type);

        // hard block ports are only represented in nodes that are either a LUT or flip flop block
        if ((curr_module_node_type.compare((module_hard_block_node_refs_and_info->target_device_info).lut_type_name) == 0) || (curr_module_node_type.compare((module_hard_block_node_refs_and_info->target_device_info).dff_type_name) == 0))
        {

            curr_module_node_info = extract_hard_block_port_info_from_module_node(curr_module_node, hard_block_type_name_list);
            
            // check to see that the current node represents a hard block port
            // a hard block instance name would not have bee found if it wasn't a hard block port
            if (!curr_module_node_info.hard_block_name.empty())
            {
                
                // if we are here, the current node is a LUT or DFF node that represents a hard block instance port //

                /* referring to the diagram at the top, if the node is a lut that represents an output port, its connected net is an internal connection, which is not a legal netlist connection, so we cannot process the node any further. Therefore we only process nodes that are LUTs representing input ports or DFFs. */
                if (is_hard_block_port_legal(curr_module_node, module_hard_block_node_refs_and_info->target_device_info))
                {

                    // get the index to the current hard block instance we need to work with
                    curr_hard_block_instance_index = find_hard_block_instance(module_hard_block_node_refs_and_info, &curr_module_node_info);

                    // check to see whether the hard block instance the current node belongs to exists (remember each node here represents a hard block port)
                    if (curr_hard_block_instance_index == HARD_BLOCK_INSTANCE_DOES_NOT_EXIST)
                    {
                        // we need to create a new hard block instance, since it does not exists for the port the current node represents
                        curr_hard_block_instance_index = create_new_hard_block_instance(node_list_with_hard_blocks, module_hard_block_node_refs_and_info, &curr_module_node_info);
                    }
                    
                    /* the remaining steps below assign the net currently connected to the node being processed (a flip-flop or a LUT) to the hard block instance port the current node represents. The correspponding hard block instance and the specific port were found previously.*/
                    assign_net_to_hard_block_instance_port(curr_module_node, &curr_module_node_info, module_hard_block_node_refs_and_info, curr_hard_block_instance_index);
                }

                /* in this if clause, the current node represents a hard block port, so we cannot have this node be written to the output netlist, so we need to add it to the nodes to delete list */
                module_hard_block_node_refs_and_info->luts_dffeas_nodes_to_remove.push_back(curr_module_node);
                
            }
        }
    }

    // update the node list for the current module
    main_module->number_of_nodes = node_list_with_hard_blocks->array_size;
    main_module->array_of_nodes = (t_node**)(node_list_with_hard_blocks->pointer);

    // we also need to delete the dynamic memory we created
    vtr::free(node_list_with_hard_blocks);

    return;
    
}

/**
 * @details This function creates an array of unconnected
 *          ports for a hard block. Additionally, a 
 *          't_hard_block_port_info' structure is created (found
 *          in 'hard_block_recog.h') to store the unconnected
 *          port array; so that the port information can be 
 *          found given a hard block type.
 * 
 * @param hard_block_arch_model This is a 't_model' structure (found in
 *                              'logic_types.h') and contains information
 *                              about a hard block within the FPGA 
 *                              architecture. 
 * 
 * @param storage_of_hard_block_info This is a data structure of type
 *                                   't_hard_block_recog', which can be found
 *                                   in 'hard_block_recog.h'. The port
 *                                   information of the hard blocks are stored
 *                                   in here.
 * 
 * @return A boolean value is returned indicating whether the hard block within
 *         the FPGA had any ports.
 * 
 */
static bool create_and_initialize_all_hard_block_ports(const t_model& hard_block_arch_model, t_hard_block_recog* storage_of_hard_block_info)
{
    int hard_block_port_index = 0;
    std::string hard_block_arch_model_name = hard_block_arch_model.name;
    bool result = true;

    // get the hard block ports
    t_model_ports* input_ports = hard_block_arch_model.inputs;
    t_model_ports* output_ports = hard_block_arch_model.outputs;

    //initialize a hard block node port array
    create_hard_block_port_info_structure(storage_of_hard_block_info,hard_block_arch_model_name);

    // handle input ports
    hard_block_port_index += extract_and_store_hard_block_model_ports(storage_of_hard_block_info, input_ports, hard_block_arch_model_name,hard_block_port_index, INPUT_PORTS);

    // handle output ports
    hard_block_port_index += extract_and_store_hard_block_model_ports(storage_of_hard_block_info, output_ports, hard_block_arch_model_name,hard_block_port_index, OUTPUT_PORTS);

    // check to see if the hard block has no ports
    if (hard_block_port_index == HARD_BLOCK_WITH_NO_PORTS)
    {

        result = false;

    }

    return result; 
}

/**
 * @details Creates and initializes a 't_hard_block_port_info'
 *          structure. This structure is then stored within
 *          the 't_hard_block_recog' structure. 
 * 
 * @param storage_of_hard_block_info This is a data structure of type
 *                                   't_hard_block_recog', which can be found
 *                                   in 'hard_block_recog.h'. The port
 *                                   information of the hard blocks are stored
 *                                   in here.
 * 
 * @param hard_block_type_name The name of a hard block in the architecture
 *                             file. Will be used to index the structure 
 *                             created in here when storing it inside
 *                             the 't_hard_block_recog' structure. 
 */
static void create_hard_block_port_info_structure(t_hard_block_recog* storage_of_hard_block_info, std::string hard_block_type_name)
{
    t_hard_block_port_info curr_hard_block_port_storage;

    // initialize with default parameters
    (curr_hard_block_port_storage.hard_block_ports).pointer = NULL;
    (curr_hard_block_port_storage.hard_block_ports).allocated_size = 0;
    (curr_hard_block_port_storage.hard_block_ports).array_size = 0;
    
    /* store the newly created structure. Since this structure will 
       preserve port information for a single hard block type within
       the FPGA architecture, we will use the corresponding hard block
       name as the index when storing this newly created structure.
    */
    storage_of_hard_block_info->hard_block_type_name_to_port_info.insert({hard_block_type_name,curr_hard_block_port_storage});

    return;

}

/**
 * @details For a given hard block in the architecture, this function converts
 *          the ports of the hard block from 't_model_ports' to a 't_array_ref'
 *          type. Then the converted ports are stored.
 * 
 *          The port information for all new types of hard blocks
 *          can be found within the FPGA architecture. For each hard block,
 *          the ports info can be found in a structure called 't_model_ports'
 *          (found in 'logic_types.h'). The ports are arranged in a linked lsit
 *          structure.
 * 
 *          THis function goes through the 't_model_ports' and stores them
 *          within a 't_node_port_association' structure (found in 'vqm_dll.h').
 *          And instead of a linked list, the ports are stored in an array
 *          within a 't_array_ref' structure (found in 'vqm_dll.h'). Finally, 
 *          the port array is stored within the 't_hard_block_port_info'
 *          (found in 'hard_block_recog.h') that is associated to the specific
 *          hard block the ports belong to. 
 * 
 *          We need to do this conversion because the hard blocks that will
 *          eventually added to the netlist will be of type 't_node' and they
 *          store ports in the form of a 't_node_port_association' array. 
 *          Additionally, for ports that are bussed, initially, they were 
 *          represented as a single 't_model_port' structure with the size
 *          of the port included, whereas they need to be seperated into
 *          individual ports (which is also done here). 
 * 
 * @param storage_of_hard_block_info This is a data structure of type
 *                                   't_hard_block_recog', which can be found
 *                                   in 'hard_block_recog.h'. The port
 *                                   information of the hard blocks are stored
 *                                   in here.
 *  
 * @param curr_hard_block_model_port This is a 't_model_port' structure (found
 *                                   in 'logic_types.h') that represents a port
 *                                   for a hard block.
 *  
 * @param curr_hard_block_type_name A string that represents the specific type 
 *                                  of hard block the ports that are being
 *                                  converted too belong to.
 * 
 * @param port_index A tracker variable that indexes each port within the
 *                   port array. For example, if we had 3 ports (test, test_1
 *                   , test_2) and they were sized 1,3,5 respectively. Then
 *                   after processing test, port index will be 0, then after
 *                   processing test_1, port_index will be 1, and finally
 *                   port_index will be 5. port index identifies the index at
 *                   which each port starts.
 *  
 * @param port_type A string that describes whether the ports that are
 *                  being processed are input or output ports.
 *  
 */
static int extract_and_store_hard_block_model_ports(t_hard_block_recog* storage_of_hard_block_info, t_model_ports* curr_hard_block_model_port, std::string curr_hard_block_type_name,int port_index, std::string port_type)
{
    t_array_ref* equivalent_hard_block_node_port_array = NULL;
    int starting_port_index = port_index;

    /* 't_model_ports' is setup as a linked list structure.
        So iterate through the list.*/
    while (curr_hard_block_model_port != NULL)
    {
        // convert the current port from the 't_model_port' to 't_node_port_association structure. and store it inside an array. 
        equivalent_hard_block_node_port_array = convert_hard_block_model_port_to_hard_block_node_port(curr_hard_block_model_port);

        // take the converted port array from above and store it
        store_hard_block_port_info(storage_of_hard_block_info, curr_hard_block_type_name, curr_hard_block_model_port->name, &equivalent_hard_block_node_port_array, &port_index);

        curr_hard_block_model_port = curr_hard_block_model_port->next;

    }

    // check to see whether the hard block has no input or output ports
    if (starting_port_index == port_index)
    {
        VTR_LOG_WARN("Model '%s' found in the architecture file does not have %s ports\n", curr_hard_block_type_name.c_str(), port_type.c_str());
    }

    return port_index;
}

/**
 * @details Given a port for hard block in the form of a
 *          't_model_port' structure, this function creates an
 *          equivalent port in the form of a 't_node_port_association'
 *          structure. The created ports are then stored within an 
 *          array.  
 * 
 * @param hard_block_model_port This is a 't_model_port' structure (found
 *                              in 'logic_types.h') that represents a port
 *                              for a hard block.
 * 
 */
static t_array_ref* convert_hard_block_model_port_to_hard_block_node_port(t_model_ports* hard_block_model_port)
{
    t_node_port_association* curr_hard_block_node_port = NULL;
    t_array_ref* port_array = NULL;
    char* curr_hard_block_model_port_name = hard_block_model_port->name;
    int port_size = hard_block_model_port->size;

    //create memory to store the port array
    port_array = create_and_initialize_t_array_ref_struct();
    
    // if a port is a bus, we need to create seperate port structures for each and every signal of the bus
    for (int port_index = 0; port_index < port_size; port_index++)
    {
        // hard blocks will not have indexed wire assignments 
        // doesnt do anything different I think
        curr_hard_block_node_port = create_unconnected_node_port_association(curr_hard_block_model_port_name, port_index, PORT_WIRE_NOT_INDEXED);

        // store the newly created specific port index within the entire port
        // array
        append_array_element((intptr_t)curr_hard_block_node_port, port_array);
    }

    // handle the case where the port is not bussed
    if (port_size == PORT_NOT_BUS)
    {
        curr_hard_block_node_port->port_index = PORT_WIRE_NOT_INDEXED;
    }

    return port_array;

}

/**
 * @details Creates a unconnected hard block port in the form of
 *          a 't_node_port_association' structure (found in 
 *          'vqm_dll.h'). 
 * 
 * @param port_name A string describing the name of the port to be
 *                  created.
 * 
 * @param port_index If the port is a bus, then this parameter specifies
 *                   the index within the bus that the port will be
 *                   created for.
 * 
 * @param wire_index If the assigned net to this port is a bus, then this
 *                   parameter specifies the index of the net that is
 *                   associated with this port.
 * 
 */
static t_node_port_association* create_unconnected_node_port_association(char *port_name, int port_index, int wire_index)
{
    // allocate memory for the port
    t_node_port_association* curr_hard_block_node_port = NULL;
    curr_hard_block_node_port = (t_node_port_association*)vtr::malloc(sizeof(t_node_port_association));
    
    // initialize all the port parameters
    curr_hard_block_node_port->port_name = (char*)vtr::malloc(sizeof(char) * (strlen(port_name) + 1));
    strcpy(curr_hard_block_node_port->port_name, port_name);

    curr_hard_block_node_port->port_index = port_index;
    curr_hard_block_node_port->associated_net = NULL;
    curr_hard_block_node_port->wire_index = wire_index;

    return curr_hard_block_node_port;
}

/**
 * @details  All the port information for a given hard block
 *           is stored inside an array that is located within
 *           a 't_hard_block_port_info' structure (found in 
 *           'hard_block_recog.h'). This function takes a single
 *           port and appends it to an array of other ports that
 *           belong to the hard block. This function also stores the
 *           index of where the previously appended port begins within
 *           the port array.
 * 
 * @param storage_of_hard_block_port_info This is a data structure of type
 *                                   't_hard_block_recog', which can be found
 *                                   in 'hard_block_recog.h'. The port
 *                                   information of the hard blocks are stored
 *                                   in here.
 * 
 * @param curr_hard_block_type_name A string that represents the name of the
 *                                  hard block type the port to store belongs
 *                                  to.
 * 
 * @param curr_port_name A string that represents the name of the current
 *                       port to store. 
 *  
 * @param curr_port_array A 't_array_def' structure (found in 'vqm_dll.h')
 *                        that contains the current port to store.
 * 
 * @param port_index A integer that defines the index within the port array
 *                   that the current port is located at.
 * 
 */
static void store_hard_block_port_info(t_hard_block_recog* storage_of_hard_block_port_info, std::string curr_hard_block_type_name,std::string curr_port_name, t_array_ref** curr_port_array, int* port_index)
{
       
    std::unordered_map<std::string,t_hard_block_port_info>::iterator curr_port_info = ((storage_of_hard_block_port_info->hard_block_type_name_to_port_info).find(curr_hard_block_type_name));

    // appending the current port to the array of ports for the current hard block 
    copy_array_ref(*curr_port_array, &(curr_port_info->second).hard_block_ports);
    
    // insert the port name to the current index
    (curr_port_info->second).port_name_to_port_start_index.insert(std::pair<std::string,int>(curr_port_name, *port_index));

    // update the index that the next port will begin at
    *port_index += (*curr_port_array)->array_size;

    // store the current port size
    (curr_port_info->second).port_name_to_port_size.insert(std::pair<std::string,int>(curr_port_name, (*curr_port_array)->array_size));

    /* we don't need to the current port anymore, since it is appended to the array of ports for the corresponding hard block. So delete the memory for it.*/
    vtr::free((*curr_port_array)->pointer);
    vtr::free(*curr_port_array);

    *curr_port_array = NULL;

    return;
}

/**
 * @details Given two 't_array_ref' structures (found in 'vqm_dll.h'),
 *          this function copies the array contents from one structure and
 *          appends it the other structure.
 * 
 * @param array_ref_orig The 't_array_ref' structure that will have its
 *                       array contents copied.
 * 
 * @param array_ref_copy The 't_array_ref' structure that will have copied
 *                       contents added to its array.
 */
static void copy_array_ref(t_array_ref* array_ref_orig, t_array_ref* array_ref_copy)
{
    int array_size = array_ref_orig->array_size;

    // append the array elements from one 't_array_ref' structure to the other
    for (int index = 0; index < array_size; index++)
    {
        append_array_element((intptr_t)array_ref_orig->pointer[index], array_ref_copy);
    }

    return;
}

/**
 * @details Creates and initializes an empty 't_array_ref' structure.
 * 
 */
static t_array_ref* create_and_initialize_t_array_ref_struct(void)
{
    t_array_ref* empty_array = NULL;

    empty_array = (t_array_ref*)vtr::malloc(sizeof(t_array_ref));

    // initially the 't_array_ref' structure has no contents.
    // Making sure that the structure parameters reflect that.s
    empty_array->pointer = NULL;
    empty_array->array_size = 0;
    empty_array->allocated_size = 0;

    return empty_array;

}

/**
 * @details Every time a hard block instance is added to the module
 *          (netlist), a reference is stored inside a list of
 *          hard blocks within the design. This function returns
 *          an index to a hard block instance within the list of stored
 *          hard blocks in the module using the hard block instance name. 
 * 
 * @param module_hard_block_node_refs_and_info This is a data structure of type
 *                                   't_hard_block_recog', which can be found
 *                                   in 'hard_block_recog.h'. This datastructure
 *                                   stores a list of references to the hard 
 *                                   block instances within the design. 
 *                                   This parameter stores a map data structure
 *                                   that associates hard block instance names 
 *                                   to their index in the list. 
 * 
 * @param curr_module_node_info This is a 't_hard_block_port_info'
 *                              structure and can be found in 
 *                              'hard_block_recog.h'. This structure
 *                              stores the port information about 
 *                              the hard block type the current
 *                              instance is of. In this function, this
 *                              parameter is used to determine the hard
 *                              block instance name and the type of hard
 *                              block.
 * 
 */
static int find_hard_block_instance(t_hard_block_recog* module_hard_block_node_refs_and_info, t_parsed_hard_block_port_info* curr_module_node_info)
{
    int hard_block_instance_index = 0;

    std::unordered_map<std::string,int>::iterator curr_hard_block_instance_index_ref;

    std::string curr_hard_block_instance_name = curr_module_node_info->hard_block_name;

    /* if we previously found a module node that represented a different port of the same hard block instance, we would have already created the node to represent the hard block instance.

    Search the current list of already created hard block instances for the hard block instance the current node is a part (remember that the current node represents a port for a hard block).
    We are using hard block names to idetify each instance.*/
    curr_hard_block_instance_index_ref = module_hard_block_node_refs_and_info->hard_block_instance_name_to_index.find(curr_hard_block_instance_name);

    // now check the search result to see whether the hard block instance the current node is a part of was already created (if it exists in our internal list)
    if (curr_hard_block_instance_index_ref == module_hard_block_node_refs_and_info->hard_block_instance_name_to_index.end())
    {
        // if we are here, then the hard block instance the current node belongs to does not exist
        // return unique identifier
        hard_block_instance_index = HARD_BLOCK_INSTANCE_DOES_NOT_EXIST;
        
    }
    else
    {
        // if we are here then the the hard block instance the current node is a port of already exists. 
        //so we store its index to identify it from all the other hard block instaces found in the netlist (all hard block instances are stored in a vector within 't_hard_block_recog')
        hard_block_instance_index = curr_hard_block_instance_index_ref->second;
    }


    return hard_block_instance_index;

}

/**
 * @details This function creates a new hard block instance
 *          node ('t_node' structure). The newly created node is
 *          added to the module (netlist). A reference to the node
 *          is then stored for future access.
 * 
 * @param module_node_list This is a 't_array_def' structure that stores
 *                         all the nodes within the module (netlist) of the
 *                         design. 
 *  
 * @param module_hard_block_node_refs_and_info This is a data structure of type
 *                                  't_hard_block_recog', which can be found
 *                                  in 'hard_block_recog.h'. This data structure
 *                                  stores a list of references to the hard 
 *                                  block instances within the design. 
 *                                 
 * @param curr_module_node_info This is a 't_hard_block_port_info'
 *                              structure and can be found in 
 *                              'hard_block_recog.h'. This structure
 *                              stores the port information about 
 *                              the hard block type the current
 *                              instance is of. In this function, this
 *                              parameter is used to determine the hard
 *                              block instance name and the type of hard
 *                              block.
 * 
 * @return An integer that represents an index within a list of hard block
 *         instances in the netlist (module) that represents the position 
 *         of a hard block instance that was newly added within this function. 
 * 
 */
static int create_new_hard_block_instance(t_array_ref* module_node_list, t_hard_block_recog* module_hard_block_node_refs_and_info, t_parsed_hard_block_port_info* curr_module_node_info)
{
    // index to to the new 't_hard_block' struct being created here that is found within the 'hard_block_instances' vector
    int created_hard_block_instance_index = 0;

    // storage for the newly created hard block ports
    t_array_ref* created_hard_block_instance_ports = NULL;

    // storage for the new node within the current module that represents the new hard block instance being created here
    t_node* hard_block_instance_node = NULL;

    //get the port information for the new hard block instances type
   t_hard_block_port_info* port_info_for_curr_hard_block_type = &((module_hard_block_node_refs_and_info->hard_block_type_name_to_port_info.find(curr_module_node_info->hard_block_type))->second);

    // create a new set of ports for the new hard block instance
    created_hard_block_instance_ports = create_unconnected_hard_block_instance_ports(port_info_for_curr_hard_block_type);

    // create a new node for the new hard block instance
    hard_block_instance_node = create_new_hard_block_instance_node(created_hard_block_instance_ports, curr_module_node_info);

    //store the new hard block instance information created above within a 't_hard_block' struct and get the index to where it is stored
    created_hard_block_instance_index = store_new_hard_block_instance_info(module_hard_block_node_refs_and_info, port_info_for_curr_hard_block_type, hard_block_instance_node, curr_module_node_info);

    // append the newly created node that represents the new hard block instance to the node list for the module
    append_array_element((intptr_t)hard_block_instance_node, module_node_list);

    // delete the array ref struct used to store the reference to the newly created hard block instance ports
    vtr::free(created_hard_block_instance_ports);

    return created_hard_block_instance_index;
}

/**
 * @details This function takes a lut or dff node that represents a hard
 *          block port and takes the net connected to it and assigns it
 *          to the corresponding port in the hard block instance node in the 
 *          module (netlist).
 * 
 *          The following steps are performed:
 *          - The lut or dff node is processed to find the port within those
 *            nodes that is connected to an external net.
 *          - The corresponding hard block instance port that is represented by
 *            the lut or dff is then found.
 *          - Finally, the net connected to the lut or dff port found previously
 *            is then assigned to the hard block instance port.
 * 
 * @param curr_module_node A 't_node' structure (found in 'vqm_dll.h')
 *                         that represents a lut or dff within the module
 *                         (netlist).
 * 
 * @param curr_module_node_info This is a 't_hard_block_port_info'
 *                              structure and can be found in 
 *                              'hard_block_recog.h'. This structure
 *                              stores the port information about 
 *                              the hard block type the current
 *                              instance is of. In this function, this
 *                              parameter is used to determine the hard
 *                              block instance name and the type of hard
 *                              block.
 * 
 * @param module_hard_block_node_refs_and_info This is a data structure of type
 *                                  't_hard_block_recog', which can be found
 *                                  in 'hard_block_recog.h'. This data structure
 *                                  stores a list of references to the hard 
 *                                  block instances within the design.
 * 
 * @param curr_hard_block_instance_index an integer that identifies a single
 *                                       hard block instance within the list
 *                                       of all hard blocks in the netlist. This
 *                                       parameter identifies the hard block
 *                                       that contains the corresponding
 *                                       lut or dff node.
 * 
 */
static void assign_net_to_hard_block_instance_port(t_node* curr_module_node, t_parsed_hard_block_port_info* curr_module_node_info, t_hard_block_recog* module_hard_block_node_refs_and_info, int curr_hard_block_instance_index)
{
    t_hard_block* curr_hard_block_instance = &(module_hard_block_node_refs_and_info->hard_block_instances[curr_hard_block_instance_index]);

    std::unordered_map<std::string,t_hard_block_port_info>::iterator curr_hard_block_type_port_info = module_hard_block_node_refs_and_info->hard_block_type_name_to_port_info.find(curr_module_node_info->hard_block_type);

    int port_to_assign_index = 0;

    t_node_port_association* curr_module_node_port_connected_to_hard_block_instance_net = get_lut_dffeas_port_connected_to_hard_block_instance_net(curr_module_node, module_hard_block_node_refs_and_info->target_device_info);

    port_to_assign_index = identify_port_index_within_hard_block_type_port_array(&(curr_hard_block_type_port_info->second), curr_module_node_info, curr_module_node);

    // assign the net to the crresponding hard block instance port //
    handle_net_assignment(curr_module_node, curr_hard_block_instance, port_to_assign_index, curr_module_node_port_connected_to_hard_block_instance_net, curr_module_node_info);

    return;
}

/**
 * @details The luts and dff that represent hard block ports
 *          generally contain a number of ports themselves. This
 *          function determines the port within the luts and dff
 *          that is connected to an external (we need to connect this
 *          net to the hard block port).
 * 
 *          We find that for luts, there are generally two ports, an
 *          input port (dataa, datab, datac, datad, datae, dataf) and 
 *          and output port (combout). When processing luts, this function
 *          returns the port that is not named 'combout'.
 * 
 *          When looking at dff, there are three ports (clk, d, q). When 
 *          processing dff, this function returns the 'q' port.
 * 
 * @param curr_module_node A 't_node' structure (found in 'vqm_dll.h')
 *                         that represents a lut or dff within the module
 *                         (netlist).
 * 
 */
static t_node_port_association* get_lut_dffeas_port_connected_to_hard_block_instance_net(t_node* curr_module_node, DeviceInfo target_device_info)
{
    t_node_port_association* port_connected_to_hard_block_instance_net = NULL;

    int number_of_ports_in_node = curr_module_node->number_of_ports;

    std::string curr_module_node_type = curr_module_node->type;
    std::string curr_module_node_name = curr_module_node->name;

    std::string curr_port_name;

    // store what type of port we expect the net to be connected to 
    std::string expected_port_type;

    if (!(curr_module_node_type.compare(target_device_info.lut_type_name)))
    {

        // we are here if the current node is LUT

        // if the current node is a LUT, then the LUT represents an input port and so the LUT input should be connected to the net
        expected_port_type.assign(INPUT_PORTS);
    }
    else
    {
        // we are here if the current node is a DFF

        // if the current node is a DFF, then the DFF represents an output port and so the DFF output should be connected to the net
        expected_port_type.assign(OUTPUT_PORTS);
    }

    for (int i = 0; i < number_of_ports_in_node; i++)
    {

        curr_port_name.assign(curr_module_node->array_of_ports[i]->port_name);

        if (!(curr_module_node_type.compare(target_device_info.lut_type_name)))
        {
            // if the node is a LUT //
            if (curr_port_name.compare(target_device_info.lut_output_port))
            {
                // if the port is not the LUT output ie. "combout" port //

                /* if we are here then we know that the LUT the current node
                represents must be an input port for hard block instance it is part of. We also know that this type of LUT only has an input and output port in the vqm netlist. Finally we know that the current port must be an input, so it's connected net is what we want to assign to the hard block instance port. */
                port_connected_to_hard_block_instance_net = curr_module_node->array_of_ports[i];

                break;
            }

        }
        else
        {
            // if the node is a flip flop //
            if (!(curr_port_name.compare(target_device_info.dff_output_port)))
            {
                // if the port is a D flip flop output ie. "q" port //

                /* if we are here then we know that the DFF the current node
                represents mus be an output port for hard block instance it is part of. Finally we know that the current port must be an output, so its connected net is what we want to assign to the hard block instance port. */
                port_connected_to_hard_block_instance_net = curr_module_node->array_of_ports[i];

                break;
            }
        }
    }

    if (port_connected_to_hard_block_instance_net == NULL)
    {
         /* we did not find any ports that fit the conditions above
        and this means that the current node, which represents a hard block 
        instance port does not have an accompanying net. This should never happen, so throw an error and indicate to the user */
        throw vtr::VtrError("The vqm netlist node '" + curr_module_node_name + "' does not have an " + expected_port_type + " port.");
    }
    
    return port_connected_to_hard_block_instance_net;
}

/**
 * @details All the ports of a hard block are arranged within
 *          an array that is inside a t_array_ref structure.  
 *          This function returns the index of where a port
 *          is located within the array.
 * 
 * @param curr_hard_block_type_port_info This is a 't_hard_block_port_info'
 *                                       structure and can be found in 
 *                                       'hard_block_recog.h'. This structure
 *                                       stores the port information about 
 *                                       the hard block type the current
 *                                       instance is of. In this function, this
 *                                       parameter is used to determine the
 *                                       indices of ports within the port
 *                                       array of a hard block.
 * 
 * @param curr_module_node_info This is a 't_hard_block_port_info'
 *                              structure and can be found in 
 *                              'hard_block_recog.h'. This structure
 *                              stores the port information about 
 *                              the hard block type the current
 *                              instance is of. In this function, this
 *                              parameter is used to determine the hard
 *                              block instance name and the type of hard
 *                              block.
 * 
 * @param curr_module_node A 't_node' structure (found in 'vqm_dll.h')
 *                         that represents a lut or dff within the module
 *                         (netlist). Used to report error information.
 * 
 */
static int identify_port_index_within_hard_block_type_port_array(t_hard_block_port_info* curr_hard_block_type_port_info, t_parsed_hard_block_port_info* curr_module_node_info, t_node* curr_module_node)
{
    int identified_port_index = 0;
    
    int port_end_index = 0;

    // error related identifiers
    std::string curr_module_node_name = curr_module_node->name;
    
    // identifier to store the port size of the current port
    std::unordered_map<std::string,int>::iterator found_port_size;

    /* use the mapping structure within the hard block port info struct
    to find the index the port can be found in within the port array of a given hard block. If the port is vectored, then the index returned is the beginning of the vectored port (ie. vectored_port[0])*/
    std::unordered_map<std::string,int>::iterator found_port_start_index = curr_hard_block_type_port_info->port_name_to_port_start_index.find(curr_module_node_info->hard_block_port_name);

    // check to see whether the port name exists
    if (found_port_start_index == (curr_hard_block_type_port_info->port_name_to_port_start_index.end()))
    {
        // port does not exist, so throw and error and indicate it to the user
        throw vtr::VtrError("The vqm netlist node '" + curr_module_node_name + "' represents a port: '" + curr_module_node_info->hard_block_port_name +"' within hard block model: '" + curr_module_node_info->hard_block_type + "'. But this port does not exist within the given hard block model as described in the architecture file.");
    }


    // at this point the port belongs to the hard block model //

    /* now we assign the found base port index and increment it by the parsed index from the node name (ie hard_block_port_index from t_parsed_hard_block_port_info). 
    
    For example support a port array is arranged as follows:
    index:         0           1           2            3
    port_array: port_1[0]  port_1[1]   port_1[2]    port_1[2]

    Now if we want the index of port_1[1], the base port index would be 0, then we need to increment by 1. This is why the increment is requried.

    If the port is not vectored, the increment value would be 0. So the case is handled
    */
   identified_port_index = found_port_start_index->second;
   identified_port_index += curr_module_node_info->hard_block_port_index;

   // now we check whether the port index is within the port range //

   // if the port size of the current port (using internal mapping structure found in the hard block port info struct)
   // this should result in a valid value as we already verfied whether the port exists
   found_port_size = curr_hard_block_type_port_info->port_name_to_port_size.find(curr_module_node_info->hard_block_port_name);

   // calculate port end index
   port_end_index = found_port_start_index->second + (found_port_size->second - 1);

   // verify if the current port index is out of ranged by chekcing if it is larger than the maximum index for the port
   if (identified_port_index > port_end_index)
   {
       // port index is out of range, so throw an error and indicate it to the user
        throw vtr::VtrError("The vqm netlist node '" + curr_module_node_name + "' represents a port: '" + curr_module_node_info->hard_block_port_name +"' at index: " + std::to_string(curr_module_node_info->hard_block_port_index) + ", within hard block model: '" + curr_module_node_info->hard_block_type + "'. But this port index is out of range in the given hard block model as described in the architecture file.");
   }

   return identified_port_index;

}

/**
 * @details This function connects a port of a given hard block instance node
 *          to a net within the netlist. The net here is a 't_pin_def'
 *          structure (found in 'vqm_dll.h'). 
 * 
 * @param curr_module_node  't_node' structure
 *                         (found in 'vqm_dll.h') that represents
 *                         a lut or dff node within the module (netlist).
 *                         This is used to get the node name.
 * 
 * @param curr_hard_block_instance a 't_hard_block' structure that represents
 *                                 a hard block instance.
 * 
 * @param port_to_assign_index The specific port within the port array that we
 *                             need to connect the net to. 
 * 
 * @param port_connected_to_hard_block_instance_net This is a 
 *                                          't_node_port_association' structure
 *                                          that represents a port that is 
 *                                          currently connected to the net we
 *                                          want to assign to the hard block.
 *                                          This port is part of a lut or dff
 *                                          node.
 * 
 * @param curr_module_node_info This is a 't_hard_block_port_info'
 *                              structure and can be found in 
 *                              'hard_block_recog.h'. This structure
 *                              stores the port information about 
 *                              the hard block type the current
 *                              instance is of. In this function, this
 *                              parameter is used to determine the hard
 *                              block instance name and the type of hard
 *                              block.
 */
static void handle_net_assignment(t_node* curr_module_node, t_hard_block* curr_hard_block_instance, int port_to_assign_index, t_node_port_association* port_connected_to_hard_block_instance_net, t_parsed_hard_block_port_info* curr_module_node_info)
{
    std::string curr_module_node_name = curr_module_node->name;

    t_pin_def* net_to_assign = port_connected_to_hard_block_instance_net->associated_net;

    t_node_port_association* port_to_assign = curr_hard_block_instance->hard_block_instance_node_reference->array_of_ports[port_to_assign_index];

    // need to check whether the current port was previouly assigned to a net
    if ((port_to_assign->associated_net) == NULL)
    {
        // port was not assigned to a net previously //

        // assign the net to the port ("connect" them)
        // since we just connected a port, we have one less port to assign, so update the "ports left to assign" tracker
        port_to_assign->associated_net = net_to_assign;
        curr_hard_block_instance->hard_block_ports_not_assigned -= 1;

        // assign the wire index of the net that we connect to the hard block port
        port_to_assign->wire_index = port_connected_to_hard_block_instance_net->wire_index;
    }
    else
    {
        // the port was already assigned previouly, this is an error, since we cannot have multiple nets connected to the same port
        // so report an error
        throw vtr::VtrError("The vqm netlist node '" + curr_module_node_name + "' represents a port: '" + curr_module_node_info->hard_block_port_name +"' within hard block model: '" + curr_module_node_info->hard_block_type + "'. But this port was already represented previously, therefore the current netlist node is a duplication of the port. A port can only have one netlist node representing it.");
    }

    return;
}

/**
 * @details Looking at the figure at the top, there are essentially
 *          two types of luts, that represents hard block ports. The first
 *          type of lut represents input ports and the second type of lut 
 *          represents an output port.
 *          
 *          This function checks to see whether a ndde is a lut that represents
 *          and output or not.
 * 
 * @param curr_module_node A 't_node' structure
 *                         (found in 'vqm_dll.h') that represents
 *                         a lut or dff node within the module (netlist).
 * 
 */
static bool is_hard_block_port_legal(t_node* curr_module_node, DeviceInfo target_device_info)
{

    bool result = false;

    std::string curr_module_node_type = curr_module_node->type;

    // now we check whether the current node is a lut that represents an input port or the current node is a DFF
    if ((curr_module_node->number_of_ports != target_device_info.lut_output_port_size) || (!(curr_module_node_type.compare(target_device_info.dff_type_name))))
    {
        result = true;
    }

    return result;

}

/**
 * @details This function creates a 't_array_ref' structure
 *          to store a new array of ports that represent the 
 *          connectivity of a hard block instance within the design.
 *          
 *          All the new hard blocks in the design are previously
 *          processed and a template of their ports are stored inside
 *          a 't_hard_block_port_info' (found in hard_block_recog.h') structure.
 *          By template, we mean that all the default information about the
 *          ports are stored and the ports are unconnected.
 * 
 *          In this function, the 't_array_ref' structure is initialized
 *          by copying the port information stored inside the
 *          't_hard_block_port_info' structure.
 * 
 * @param curr_hard_block_type_port_info This is a 't_hard_block_port_info'
 *                                       structure and can be found in 
 *                                       'hard_block_recog.h'. This structure
 *                                       stores the port information about 
 *                                       the hard block type the current
 *                                       instance is of. In this function, this
 *                                       parameter is used to copy the port
 *                                       information about the hard block.
 * 
 */
static t_array_ref* create_unconnected_hard_block_instance_ports(t_hard_block_port_info* curr_hard_block_type_port_info)
{
    t_array_ref* template_for_hard_block_ports = &(curr_hard_block_type_port_info->hard_block_ports);
    t_array_ref* hard_block_instance_port_array = NULL;
    int number_of_ports = template_for_hard_block_ports->array_size;

    // temporarily store single port parameters
    char* port_name = NULL;
    int port_index = 0;
    int port_wire_index = 0;

    // store the newly created port
    t_node_port_association* temp_port = NULL;

    // convert the template hard block ports into a port format
    t_node_port_association** template_port_array = (t_node_port_association**)template_for_hard_block_ports->pointer;

    // create memory to store the ports for the newly created hard block instance
    hard_block_instance_port_array = create_and_initialize_t_array_ref_struct();

    // go through the template ports, copy their parameters to a new set of ports that will be for the new hard block instance we are creating
    for (int i = 0; i < number_of_ports; i++)
    {
        port_name = template_port_array[i]->port_name;
        port_index = template_port_array[i]->port_index;
        port_wire_index = template_port_array[i]->wire_index;

        temp_port = create_unconnected_node_port_association(port_name, port_index, port_wire_index);

        append_array_element((intptr_t)temp_port, hard_block_instance_port_array);
    }

    return hard_block_instance_port_array;

}

/**
 * @details This function creates a 't_node' structure
 *          for a hard block instance that need to be added to the 
 *          netlist. The following steps are performed on the 
 *          't_node' structure:
 *          - store the array of unconnected ports to describe
 *            the connectivity of this hard block instance
 *          - store the name of the hard block instance and also its
 *            type
 * 
 * @param curr_hard_block_instance_ports A 't_array_ref' structure (found in
 *                                       'vqm_dll.h') that contains an array
 *                                       of unconnected ports that represent
 *                                       the connectivity of a hard block
 *                                       instance.
 * 
 * @param curr_hard_block_instance_info This is a 't_hard_block_port_info'
 *                                       structure and can be found in 
 *                                       'hard_block_recog.h'. This structure
 *                                       stores the port information about 
 *                                       about the hard block type the current
 *                                       instance is of. In this function, this
 *                                       parameter is used to determine the
 *                                       total number of ports in the hard
 *                                       block.
 * 
 */
static t_node* create_new_hard_block_instance_node(t_array_ref* curr_hard_block_instance_ports, t_parsed_hard_block_port_info* curr_hard_block_instance_info)
{
    t_node* new_hard_block_instance = NULL;

    char* hard_block_instance_name = NULL;
    char* hard_block_instance_type = NULL;

    int hard_block_instance_name_size = strlen((curr_hard_block_instance_info->hard_block_name).c_str()) + 1;
    int hard_block_instance_type_size = strlen((curr_hard_block_instance_info->hard_block_type).c_str()) + 1;

    // create the node for the new hard block instance
    new_hard_block_instance = (t_node*)vtr::malloc(sizeof(t_node));

    // assign the ports and their count for the new hard block
    new_hard_block_instance->array_of_ports = (t_node_port_association**)curr_hard_block_instance_ports->pointer;

    new_hard_block_instance->number_of_ports = curr_hard_block_instance_ports->array_size;

    // hard blocks will not have any parameters so indicate that
    new_hard_block_instance->number_of_params = 0;
    new_hard_block_instance->array_of_params = NULL;

    // create storage for the name and type of the current hard block and store their values //
    
    hard_block_instance_name = (char*)vtr::malloc(sizeof(char)*hard_block_instance_name_size);
    hard_block_instance_type = (char*)vtr::malloc(sizeof(char)*hard_block_instance_type_size);

    strcpy(hard_block_instance_name, (curr_hard_block_instance_info->hard_block_name).c_str());
    strcpy(hard_block_instance_type, (curr_hard_block_instance_info->hard_block_type).c_str());

    new_hard_block_instance->name = hard_block_instance_name;
    new_hard_block_instance->type = hard_block_instance_type;

    return new_hard_block_instance;

}

/**
 * @details This function performs the following steps:
 *          - Stores a reference to a newly added hard block node
 *            to the module (netlist) and as well as information
 *            about the port assignments of the hard block into a
 *            't_hard_block' datastructure (found in 'hard_block_recog.h')
 *          - The 't_hard_block' is then stored into a vector of other
 *            hard blocks in the design
 *          - Finally, the index of the 't_hard_block' structure within the
 *            previous vector is stored inside a map datastructure and the 
 *            key was the name of the hard block instance within the design
 *            and this was done for a quick lookup.
 * 
 * @param module_hard_block_node_refs_and_info This is a data structure of type
 *                                   't_hard_block_recog', which can be found
 *                                   in 'hard_block_recog.h'. The reference to
 *                                   the newly added hard block instance in the 
 *                                   design will be stored in here.
 * 
 * @param curr_hard_block_type_port_info This is a 't_hard_block_port_info'
 *                                       structure and can be found in 
 *                                       'hard_block_recog.h'. This structure
 *                                       stores the port information about 
 *                                       about the hard block type the current
 *                                       instance is of. In this function, this
 *                                       parameter is used to determine the
 *                                       total number of ports in the hard
 *                                       block.
 * 
 * @param new_hard_block_instance_node A 't_node' structure
 *                         (found in 'vqm_dll.h') that represents
 *                         a single hard block instance within the 
 *                         design.
 * 
 * @param curr_module_node_info This is a 't_parsed_hard_block_port_info'
 *                             structure. It is defined in 'hard_block_recog.h'.
 *                             This structure is created to store relevant 
 *                             information about a hard block port
 *                             whenever a node is identified as representing
 *                             a hard block port. In this function, this
 *                             parameter provides the name of the corresponding
 *                             hard block instance within the design.
 * 
 */
static int store_new_hard_block_instance_info(t_hard_block_recog* module_hard_block_node_refs_and_info, t_hard_block_port_info* curr_hard_block_type_port_info, t_node* new_hard_block_instance_node, t_parsed_hard_block_port_info* curr_module_node_info)
{
    int new_hard_block_instance_index = 0;

    t_hard_block new_hard_block_instance_info;

    // store information regarding the new hard block instance being added to the module node list to a new t_hard_block struct //

    new_hard_block_instance_info.hard_block_instance_node_reference = new_hard_block_instance_node;
    
    // initially all ports for the newly created hard block instance are unassigned 
    new_hard_block_instance_info.hard_block_ports_not_assigned = curr_hard_block_type_port_info->hard_block_ports.array_size;

    // insert the hard block (t_hard_block struct) to the list of all hard block instances
    module_hard_block_node_refs_and_info->hard_block_instances.push_back(new_hard_block_instance_info);

    // since we added the new hard block instance (t_hard_block struct) at the end of vector, the index will be the last position with the list
    new_hard_block_instance_index = module_hard_block_node_refs_and_info->hard_block_instances.size() - 1;

    /* now create a mapping between the new hard block instance name and the index it is located in with the list of all hard block instances, so we can find it quicly using just the hard block instance name*/
    module_hard_block_node_refs_and_info->hard_block_instance_name_to_index.insert(std::pair<std::string,int>(curr_module_node_info->hard_block_name,new_hard_block_instance_index));

    return new_hard_block_instance_index;
}

/**
 * @details Given an arbritary array of pointers, this function
 *          stores the array and its properties into a 
 *          't_array_ref' structure.
 * 
 * @param array_to_store array of pointers
 *  
 * @param array_size size of the array passed to this function
 * 
 */
static t_array_ref* create_t_array_ref_from_array(void** array_to_store, int array_size)
{   
    t_array_ref* array_reference = NULL;
    int array_allocated_size = 0;

    array_reference = (t_array_ref*)vtr::malloc(sizeof(t_array_ref));

    // determine how large the array is
    array_allocated_size = calculate_array_size_using_bounds(array_size);

    // assign the array and its corresponding size information to the array ref struct
    array_reference->allocated_size = array_allocated_size;
    array_reference->array_size = array_size;
    array_reference->pointer = array_to_store;

    return array_reference;

}

/**
 * @details Given a node within the module (netlist) that is a lut or dff
 *          which represents a hard block port, this function identifies
 *          the following information:
 *          - The name of the hard block instance which the current port that
 *            the node represents belongs too
 *          - The specific hard block this node is a part of (figuring out
 *            which type of hard block the port that the node represents
 *            belongs too)
 *          - The name of the port that the current node represents
 *          - If the port is a bus, then the specific index the current
 *            node represents
 * 
 * @param curr_module_node A 't_node' structure (found in 'vqm_dll.h') that
 *                         is a lut or dff which represents a port of 
 *                         hard block.
 * 
 * @param hard_block_type_name_list A list of the hard block names that need
 *                                  to be properly added to the netlist.
 * 
 */
static t_parsed_hard_block_port_info extract_hard_block_port_info_from_module_node(t_node* curr_module_node, std::vector<std::string>* hard_block_type_name_list)
{
    std::string curr_module_node_name = curr_module_node->name;

    // container to hold all the names of the different hierachy levels found for the current node in the netlist(refer to 'split_node_name function' for more info)
    std::vector<std::string> components_of_module_node_name;

    int index_of_node_name_component_with_hard_block_type_info = 0;
    int index_of_node_name_component_with_hard_block_port_info = 0;

    // data structure to hold all the extract port information
    t_parsed_hard_block_port_info stored_port_info;

    split_node_name(curr_module_node_name, &components_of_module_node_name, VQM_NODE_NAME_DELIMITER);

    // if the node name does not have atleast two hierarhcy levels, then it cannot be a hard block port so we cannot extract any more information.
    // a hard block port in the vqm netlist must have atleast the port information and the next top level block name it is connected to. As shown below:
    // For example, \router:test_noc_router|payload[8]~QIC_DANGLING_PORT_I is a hard block payload port connected to a router block.
    // \Add0~9_I is not a hard block port
    if ((components_of_module_node_name.size()) >= 2)
    {
        // looking at the comment above, regardless of how many hierarchy levels an node has, the lowest level (last index) will contain the port info and the second lowest level (second last index) will contain the hard block type.
        // so we store those indices below
        index_of_node_name_component_with_hard_block_type_info = components_of_module_node_name.size() - 2;
        index_of_node_name_component_with_hard_block_port_info = components_of_module_node_name.size() - 1;

        stored_port_info.hard_block_type.assign(identify_hard_block_type(hard_block_type_name_list, components_of_module_node_name[index_of_node_name_component_with_hard_block_type_info]));

        // if the hard block type was empty, then the current node does not represent a hard block port, therefore we cannot extract any more info
        /* For example, sha256_pc:\sha256_gen:10:sha256_pc_gen:sha256_2|altshift_taps:q_w_rtl_2|shift_taps_b8v:auto_generated|altsyncram_6aa1:altsyncram5|ram_block6a17550~I meets all the conditions to get here, but it is a ram block */
        if (!(stored_port_info.hard_block_type.empty()))
        {
            // if the hard block type was verified then we can go ahead and store its name and also its parsed port name and index

            stored_port_info.hard_block_name.assign(construct_hard_block_name(&components_of_module_node_name, VQM_NODE_NAME_DELIMITER));

            identify_hard_block_port_name_and_index(&stored_port_info, components_of_module_node_name[index_of_node_name_component_with_hard_block_port_info]);
        }
    }

    return stored_port_info;

}

/**
 * @details Given a string, this function splits the string into multiple
 *          pieces by a delimiter character.
 * 
 *          All the node names within the module (netlist) are composed of
 *          multiple elements based on a nodes 'hierarchy' within the design.
 *          
 *          For example:
 *          '\router:test_noc_router|sc_flit[0]~0_I'
 *          
 *          In the node name above, the first component is
 *          '\router:test_noc_router' and the second component
 *          is 'sc_flit[0]~0_I'. The first component in this example
 *          describes the name of the module and the second component
 *          describes the specific port of the module. We want to divide
 *          the node name into multiple components and this is done within
 *          this function.
 * 
 * @param original_node_name The name of a node within the module (netlist).
 *  
 * @param node_name_components A list of strings that are seperated components 
 *                             of the original_node_name string.
 *  
 * @param delimiter A character that will be used to seperate the
 *                  original_node_name string above into multiple
 *                  pieces.
 * 
 */
static void split_node_name(std::string original_node_name, std::vector<std::string>* node_name_components, std::string delimiter)
{

    // positional trackers to determine the beginning and end position of each hierarchy level (component of the node name) of the current node within the design. The positions are updated as we go through the node name and identify every level of hierarchy.
    size_t start_of_current_node_name_component = 0;
    size_t end_of_current_node_name_component = 0;

    // go through the node name, then identify and store each hierarchy level of the node (represented as a component of the original node name), found using the delimiter
    while ((end_of_current_node_name_component = original_node_name.find(delimiter, start_of_current_node_name_component)) != std::string::npos)
    {
        // store the current node name component (current hierarchy level)
        node_name_components->push_back(original_node_name.substr(start_of_current_node_name_component, end_of_current_node_name_component - start_of_current_node_name_component));

        // update position for the next node name component (next hierarchy level)
        start_of_current_node_name_component = end_of_current_node_name_component + delimiter.length();

    }

    // since the last component (the port info is not follwed by a delimiter we need to handle it here and store it)
    node_name_components->push_back(original_node_name.substr(start_of_current_node_name_component, end_of_current_node_name_component - start_of_current_node_name_component));

    return;
}

/**
 * @details This function iterates through a list of hard
 *          block names and checks to see if any of the
 *          names are a substring within a given t_module (netlist)
 *          node name. If a substring is found, then the matched
 *          hard block name is returned. The returned name is the
 *          basically the type of hard block this node belongs to. 
 * 
 * @param hard_block_type_name_list A list of the hard block names that need
 *                              to be properly added to the netlist.
 * 
 * @param curr_node_name_component The name of node within the t_module
 *                                 structure.
 * 
 */
static std::string identify_hard_block_type(std::vector<std::string>* hard_block_type_name_list, std::string curr_node_name_component)
{
    std::vector<std::string>::iterator hard_block_type_name_traverser;
    
    std::string hard_block_type_name_to_find;

    // stores the matched hard block name. Default is empty, meaning that
    // nothing was matched
    std::string hard_block_type = "";
    
    size_t match_result = 0;

    // iterate through each hard block name in the list
    for (hard_block_type_name_traverser = hard_block_type_name_list->begin(); hard_block_type_name_traverser != hard_block_type_name_list->end(); hard_block_type_name_traverser++)
    {   
        // adding the colon operator to the end of the current hard block name
        // ex. 'test' -> 'test:'
        /* If a node belongs to a hard block, then the hard block name is
         generally followed by  a colon operator, which is why need to append it.
         for a 'router' block we can expect something like this:
         \router:test_noc_router|sc_flit[8]~reg0_I, where router is follwed by :

         This also helps ignore invalid cases where a block instantiation has
         a hard block name within it, even though the block is not of that hard
         block type.
         For example:
            test_block router_one
        */
        hard_block_type_name_to_find.assign(*hard_block_type_name_traverser + HARD_BLOCK_TYPE_NAME_SEPERATOR);

        match_result = curr_node_name_component.find(hard_block_type_name_to_find);

        // check to see if the node belongs to the current hard block type
        // (the current hard block name is a substring within the node name)
        if (match_result != std::string::npos)
        {
            hard_block_type.assign(*hard_block_type_name_traverser);
            break;
        }

    }

    return hard_block_type;
}

/**
 * @details Given a list of strings, this function combines the strings
 *          together using a delimitter and generates a combined string.
 *          
 * @param node_name_components A list of strings that will be combined
 *                             together to generate a single string output.
 * @param delimiter A character that will be used to seperate the list elements
 *                  above in the generated string output.
 * 
 */
static std::string construct_hard_block_name(std::vector<std::string>*node_name_components, std::string delimiter)
{
    // stores the full name of the hard block the current node is part of, the current node represents a port of a hard block
    std::string curr_hard_block_name = "";

    /* the hard block name should not include the specific port the current node represents (so we reduce the size by 1 to remove it when constructing the hard block name)
    the last index of the node name components vector contains the port information, so by reducing the size of the vector by one we can ignore the port info when constructing the hard block name 
    
    For example, if the current node name for a 'router' block was:
    '\router:test_noc_router|sc_flit[8]~reg0_I'
    we know that the 'sc_flit[8]~reg0_I' is a specific port, so
    we can remove it when constructing the hard block name. Which
    would just be '\router:test_noc_router|'. */
    int number_of_node_name_components = node_name_components->size() - 1;

    // go through the node name components and combine them together to form the hard block name. 
    for (int i = 0; i < number_of_node_name_components; i++)
    {
        curr_hard_block_name += (*node_name_components)[i] + delimiter;
    }

    return curr_hard_block_name;

}

/**
 * @details Given a node in the module (netlist) that we know represents
 *          a hard block port, this function takes a component of the node name 
 *          and determines the specific hard block port and as well as the 
 *          index of the port if it is a bus. The determined values are then
 *          stored. 
 * 
 * @param curr_hard_block_port This is a 't_parsed_hard_block_port_info'
 *                             structure. It is defined in 'hard_block_recog.h'.
 *                             This structure is created to store relevant 
 *                             information about a hard block port
 *                             whenever a node is identified as representing
 *                             a hard block port.
 *  
 * @param curr_node_name_component Whenever a node is identified as representing
 *                     a hard block port, only a portion of the name
 *                     contains useful information about the hard block port.
 *                     For example, suppose a node name for a
 *                     'router' block port was:
 *                     '\router:test_noc_router|sc_flit[8]~reg0_I'
 *                     
 *                     Looking at the name above, only the
 *                     component 'sc_flit[8]~reg0_I' contains 
 *                     information above the port. So that 
 *                     component is passed for this parameter.      
 * 
 */
static void identify_hard_block_port_name_and_index (t_parsed_hard_block_port_info* curr_hard_block_port, std::string curr_node_name_component)
{   
    // identifer to check whether the port defined in the current node name is a bus (ex. payload[1]~QIC_DANGLING_PORT_I)
    std::regex port_is_a_bus ("(.*)[[]([0-9]*)\\]~(?:.*)");

    // identifier to check whether the current port defined in the current node name isn't a bus (ex. value~9490_I)
    std::regex port_is_not_a_bus ("(.*)~(?:.*)");

    // when we compare the given node name component with the previous identifiers, the port name and port index are extracted and they are stored in the variable below
    std::smatch port_info;

    // handle either the case where the current port defined in the provided node name is either a bus or not
    // we should never not match with either case below
    if (std::regex_match(curr_node_name_component, port_info, port_is_a_bus, std::regex_constants::match_default))
    {
        // if we are here, then the port is a bus

        // store the extracted port name and index to be used externally
        curr_hard_block_port->hard_block_port_name.assign(port_info[PORT_NAME]);
        
        curr_hard_block_port->hard_block_port_index = std::stoi(port_info[PORT_INDEX]);
    }
    else if (std::regex_match(curr_node_name_component, port_info, port_is_not_a_bus, std::regex_constants::match_default))
    {
        // if we are here then the port was not a bus
        // not need to update port index as the default value represents a port that is not a bus

        // store the extracted port name
        curr_hard_block_port->hard_block_port_name.assign(port_info[PORT_NAME]);
    
    }

    return;
}

/**
 * @details This function removes all the nodes in the module (netlist)
 *          that are luts and dff which represent hard block ports. After
 *          identifying and adding the necessary hard block to the netlist
 *          the lut and dff that represent the hard block ports are not
 *          required, so they need to be removed from the netlist.
 * 
 * @param main_module This contains all the netlist information, such as luts,
 *                    flip flops, and other types of blocks. All the nets within
 *                    the netlist are also included.
 * 
 * @param module_hard_block_node_refs_and_info This is a data structure of type
 *                                   't_hard_block_recog', which can be found
 *                                   in 'hard_block_recog.h'. The full list
 *                                   of lut and dff nodes that need to be 
 *                                   removed should be found here.
 * 
 */
static void remove_luts_dffeas_nodes_representing_hard_block_ports(t_module* main_module, t_hard_block_recog* module_hard_block_node_refs_and_info)
{
    // reference to the list of luts/dffeas nodes we need to remove
    std::vector<t_node*>* list_of_nodes_to_remove = &(module_hard_block_node_refs_and_info->luts_dffeas_nodes_to_remove);

    t_node** module_node_list = main_module->array_of_nodes;
    int number_of_nodes_in_module = main_module->number_of_nodes;

    // iterator to go through the list of nodes we will be removing
    std::vector<t_node*>::iterator node_to_remove;

    // go through the list of nodes we need to remove and delete them
    // from the node array within the module
    for(node_to_remove = list_of_nodes_to_remove->begin(); node_to_remove != list_of_nodes_to_remove->end(); node_to_remove++)
    {
        remove_node(*node_to_remove, module_node_list, number_of_nodes_in_module);

    }

    // we need to fix the gaps created in the node array after removing all luts/dffeas nodes previously
    reorganize_module_node_list(main_module);

    return;
}

/**
 * @details This function goes through all the newly added hard block nodes
 *          to the module (netlsit) and checks to see that every port has a
 *          valid connection to a net. If a port is unassigned for any hard
 *          block, then an error is thrown.
 * 
 *          We expect that every hard block should have all its port assigned 
 *          to a net, even if the connection 'ground' of 'vdd. Having an
 *          unconnected port implies that the generate vqm netlist was 
 *          incorrect and we check for this here. 
 * 
 * @param module_hard_block_node_refs_and_info This is a data structure of type
 *                                   't_hard_block_recog', which can be found
 *                                   in 'hard_block_recog.h'. The full list
 *                                   of newly added hard block nodes need to be
 *                                   stored in here.
 * 
 */
static void verify_hard_blocks(t_hard_block_recog* module_hard_block_node_refs_and_info)
{   

    // If we find a hard block instance that has ports unassigned, we store its information in the variables below
    t_node* incomplete_hard_block_instance = NULL;
    t_node_port_association* temp_port = NULL;
    std::string incomplete_hard_block_instance_name = "";
    std::string incomplete_hard_block_instance_type = "";
    std::string unassigned_port_name = "";

    std::vector<t_hard_block>* list_of_hard_block_instances = &(module_hard_block_node_refs_and_info->hard_block_instances);

    // we order the vector of t_hard_blocks from the largest to smallest number of ports not assigned
    std::sort(list_of_hard_block_instances->begin(), list_of_hard_block_instances->end(), sort_hard_blocks_by_valid_connections);

    /* We just need to check the first element of the previously sorted vector of hard block instances in the design to see if has any ports unassigned. If the first element has all ports connected, then this means all other hard block instances in the design have all their ports assigned (since we ordered them from largest to smallest number of ports unassigned).
    
    If we find that the the hard block instance has a port or more that is unassigned, we go through all the ports and throw an error on the first unassgined port.
    */
    if (!(list_of_hard_block_instances->empty()) && (((list_of_hard_block_instances->begin())->hard_block_ports_not_assigned) != 0))
    {
        incomplete_hard_block_instance = (list_of_hard_block_instances->begin())->hard_block_instance_node_reference;
        
        // iterate through all ports in the hard block
        for (int i = 0; i < incomplete_hard_block_instance->number_of_ports; i++)
        {
            temp_port = incomplete_hard_block_instance->array_of_ports[i];
            
            // check each port too see if it was unassigned
            if (temp_port->associated_net == NULL)
            {
                // if we are here, then the port was unassigned

                // store some information about the port that is unassgined the hard block instance it is part of
                incomplete_hard_block_instance_name.assign( incomplete_hard_block_instance->name);
                incomplete_hard_block_instance_type.assign(incomplete_hard_block_instance->type);
                unassigned_port_name.assign(temp_port->port_name);


                // we store ports that are not bussed with an index of -1, we dont want to show that to the user, so we use two different error messages for ports that are a bus and ports which are not
                if ((temp_port->port_index) == PORT_WIRE_NOT_INDEXED)
                {
                    throw vtr::VtrError("The hard block instance '" + incomplete_hard_block_instance_name + "', which is of hard block type: '"+ incomplete_hard_block_instance_type + "' has a port: '" + unassigned_port_name +"' that is unassigned. This means that the port was not included in the provided vqm netlist.");
                }
                else
                {
                    throw vtr::VtrError("The hard block instance '" + incomplete_hard_block_instance_name + "', which is of hard block type: '"+ incomplete_hard_block_instance_type + "' has a port: '" + unassigned_port_name +"[" + std::to_string(temp_port->port_index)+"]' that is unassigned. This means that the port was not found in the provided vqm netlist.");
                }            
                
            }
        }
    }

    return;

}

/**
 * @details This function deletes any memory that was allocated
 *          to store the port information of the new types of 
 *          hard blocks (internally modelling them).
 * 
 *          We need to store all the port information for each
 *          and every new type of hard block. This is done by
 *          dynamically creating 't_hard_block_port_info' structures
 *          for each hard block. We handle the deletion of this
 *          memory here.
 * 
 * @param hard_block_type_name_to_port_info_map A map 
 *                                  that contains all the 
 *                                  't_hard_block_port_info' structures,
 *                                  which store the port information
 *                                  for all the new hard blocks. This
 *                                  data structure can be found within
 *                                  't_hard_block_recog'. 
 * 
 */
static void delete_hard_block_port_info(std::unordered_map<std::string, t_hard_block_port_info>* hard_block_type_name_to_port_info_map)
{
    std::unordered_map<std::string, t_hard_block_port_info>::iterator curr_hard_block_port_info = hard_block_type_name_to_port_info_map->begin();

    while (curr_hard_block_port_info != (hard_block_type_name_to_port_info_map->end()))
    {

        int number_of_ports = (curr_hard_block_port_info->second).hard_block_ports.array_size;
    
        uintptr_t* ports_to_delete = (uintptr_t*)((curr_hard_block_port_info->second).hard_block_ports.pointer);

        deallocate_array(ports_to_delete, number_of_ports, free_port_association);

        curr_hard_block_port_info++;

    }

    return;  
}

/**
 * @details A utility function that is used with std::sort to help
 *          sort a vector of 't_hard_block' structures from largest
 *          to smallest number of unconnected ports.
 * 
 */
bool sort_hard_blocks_by_valid_connections(t_hard_block instance_one, t_hard_block instance_two)
{

    return ((instance_one.hard_block_ports_not_assigned) > (instance_two.hard_block_ports_not_assigned));

}
